/**
 * @class Ext.fx.Animator
 * Animation instance
 */

Ext.define('Ext.fx.Animator', {

    /* Begin Definitions */

    mixins: {
        observable: 'Ext.util.Observable'
    },

    requires: ['Ext.fx.Manager'],

    /* End Definitions */

    /**
     * @cfg {Number} duration
     * Time in milliseconds for the animation to last. Defaults to 250.
     */
    duration: 250,

    /**
     * @cfg {Number} delay
     * Time to delay before starting the animation. Defaults to 0.
     */
    delay: 0,

    /**
     * @cfg {String} easing
     * <p>A valid Ext.fx.Easing or Ext.fx.EasingPseudo value for the effect.
     * Easing is now calculated exclusively with the use of cubic-bezier curves and follows the
     * <a href="http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag">CSS3
     * specification for 'transition-timing-function'</a>. Defaults to ease</p>
     * <h2>Standard CSS3 Easing Values:</h2>
     * <div class="mdetail-params"><ul>
     * <li><b><tt>ease</tt></b>: The ease function is equivalent to cubic-bezier(0.25, 0.1, 0.25, 1.0).</li>
     * <li><b><tt>linear</tt></b>: The linear function is equivalent to cubic-bezier(0.0, 0.0, 1.0, 1.0).</li>
     * <li><b><tt>ease-in</tt></b>: The ease-in function is equivalent to cubic-bezier(0.42, 0, 1.0, 1.0).</li>
     * <li><b><tt>ease-out</tt></b>: The ease-out function is equivalent to cubic-bezier(0, 0, 0.58, 1.0).</li>
     * <li><b><tt>ease-in-out</tt></b>: The ease-in-out function is equivalent to cubic-bezier(0.42, 0, 0.58, 1.0)</li>
     * <li><b><tt>cubic-bezier</tt></b>: Specifies a cubic-bezier curve. The four values specify points P1 and P2 of
     * the curve as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition is invalid.</li>
     * </ul></div>
     * PseudoEasing combines multiple cubic-bezier curves and creates an Ext.fx.Animation to achieve more complex effects.
     * <h2>Extended Pseudo Easing Values:</h2>
     * <div class="mdetail-params"><ul>
     * <li><b><tt>back-in</tt></b></li>
     * <li><b><tt>back-out</tt></b></li>
     * <li><b><tt>bounce-in</tt></b></li>
     * <li><b><tt>bounce-out</tt></b></li>
     * <li><b><tt>elastic-in</tt></b></li>
     * <li><b><tt>elastic-out</tt></b></li>
     * </ul></div>
     */
    easing: 'ease',

    /**
     * @private
     */
    damper: 1,

    /**
     * @cfg {Number} iterations
     * Number of times to execute the animation. Defaults to 1.
     */
    iterations: 1,

    /**
     * Current keyframe step of the animation.
     * @property keyframeStep
     * @type Number
     */
    keyframeStep: 0,

    /**
     * @private
     */
    animKeyFramesRE: /^(from|to|\d+%?)$/,

    /**
     * @cfg {Ext.fx.target} target
     * The Ext.fx.target to apply the animation to.  If not specified during initialization, this can be passed to the applyAnimator
     * method to apply the same animation to many targets.
     */

     /**
      * @cfg {Object} keyframes
      * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always considered '0%' and 'to'
      * is considered '100%'.<b>Every keyframe declaration must have a keyframe rule for 0% and 100%, possibly defined using
      * "from" or "to"</b>.  A keyframe declaration without these keyframe selectors is invalid and will not be available for
      * animation.  The keyframe declaration for a keyframe rule consists of properties and values. Properties that are unable to
      * be animated are ignored in these rules, with the exception of 'easing' which can be changed at each keyframe. For example:
 <pre><code>
keyframes : {
    '0%': {
        left: 100
    },
    '40%': {
        left: 150
    },
    '60%': {
        left: 75
    },
    '100%': {
        left: 100
    }
}
 </code></pre>
      */
    constructor: function(config) {
        config = Ext.apply(this, config || {});
        this.config = config;
        this.id = Ext.id(null, 'ext-animator-');
        this.addEvents(
            /**
             * @event beforeanimate
             * Fires before the animation starts. A handler can return false to cancel the animation.
             * @param {Ext.fx.Animator} this
             */
            'beforeanimate',
            /**
              * @event keyframe
              * Fires at each keyframe.
              * @param {Ext.fx.Animator} this
              * @param {Number} keyframe step number
              */
            'keyframe',
            /**
             * @event afteranimate
             * Fires when the animation is complete.
             * @param {Ext.fx.Animator} this
             * @param {Date} startTime
             * @param {Date} elapsedTime
             */
            'afteranimate'
        );
        this.mixins.observable.constructor.call(this, config);
        this.timeline = [];
        this.createTimeline(this.keyframes);
        if (this.target) {
            this.applyAnimator(this.target);
        }
    },

    /**
     * @private
     */
    sorter: function (a, b) {
        return a.pct - b.pct;
    },

    /**
     * @private
     * Takes the given keyframe configuration object and converts it into an ordered array with the passed attributes per keyframe
     * or applying the 'to' configuration to all keyframes.  Also calculates the proper animation duration per keyframe.
     */
    createTimeline: function(keyframes) {
        var attrs = [],
            attr,
            to = this.to || {},
            prevMs,
            ms,
            i,
            ln,
            pct,
            anim,
            nextAnim,
            duration = this.duration;

        if (keyframes.isPseudoEasing) {
            this.isPseudoEasing = true;
        }
        for (pct in keyframes) {
            if (keyframes.hasOwnProperty(pct) && this.animKeyFramesRE.test(pct)) {
                attr = {attrs: Ext.apply(keyframes[pct], to)};
                // CSS3 spec allow for from/to to be specified.
                if (pct == "from") {
                    pct = 0;
                }
                else if (pct == "to") {
                    pct = 100;
                }
                // convert % values into integers
                attr.pct = parseInt(pct, 10);
                attrs.push(attr);
            }
        }
        // Sort by pct property
        attrs.sort(this.sorter);
        // Only an end
        //if (attrs[0].pct) {
        //    attrs.unshift({pct: 0, attrs: element.attrs});
        //}

        ln = attrs.length;
        for (i = 0; i < ln; i++) {
            prevMs = (attrs[i - 1]) ? duration * (attrs[i - 1].pct / 100) : 0;
            ms = duration * (attrs[i].pct / 100);
            this.timeline.push({
                duration: ms - prevMs,
                attrs: attrs[i].attrs
            });
        }
    },

    /**
     * Applies animation to the Ext.fx.target
     * @param target
     * @type string/object
     */
    applyAnimator: function(target) {
        target = Ext.fx.Manager.createTarget(target);
        var anims = [],
            anim,
            timeline = this.timeline,
            reverse = this.reverse,
            isPseudoEasing = this.isPseudoEasing,
            ln = timeline.length,
            easing,
            damper,
            initial,
            attrs,
            lastAttrs,
            i;

        if (this.fireEvent('beforeanimate', this) !== false) {
            for (i = 0; i < ln; i++) {
                anim = timeline[i];
                attrs = anim.attrs;
                easing = attrs.easing || this.easing;
                damper = attrs.damper || this.damper;
                delete attrs.easing;
                delete attrs.damper;
                anim = new Ext.fx.Anim({
                    target: target,
                    easing: easing,
                    damper: damper,
                    initialFrom: isPseudoEasing && initial,
                    duration: anim.duration,
                    reverse: reverse,
                    paused: true,
                    from: lastAttrs,
                    to: attrs
                });
                if (!i) {
                    anim.initAttrs();
                    initial = anim.currentAttrs;
                }
                if (!isPseudoEasing) {
                    lastAttrs = Ext.apply({}, attrs, lastAttrs);
                }
                anims.push(anim);
            }

            if (reverse) {
                anims.reverse();
            }

            for (i = 0; i < ln - 1; i++) {
                anim = anims[i];
                anim.nextAnim = anims[i + 1];
                anim.on('afteranimate', function() {
                    this.nextAnim.paused = false;
                });
                anim.on('afteranimate', function() {
                    this.fireEvent('keyframe', this, ++this.keyframeStep);
                }, this);
            }
            anims[ln - 1].on('afteranimate', function() {
                this.fireEvent('afteranimate', this, this.startTime, new Date() - this.startTime);
            }, this);

            Ext.defer(function(anim) {
                this.startAnimator(anim);
            }, this.delay, this, [anims[0]]);
        }
    },

    /**
     * @private
     */
    startAnimator: function(anim) {
        this.startTime = new Date();
        anim.paused = false;
    }
});
